/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "mkdir-parents.h"
#include "eacces-error.h"
#include "unlink-old-files.h"
#include "settings.h"
#include "mailbox-uidvalidity.h"
#include "mailbox-list-private.h"
#include "maildir-storage.h"
#include "maildir-uidlist.h"
#include "maildir-keywords.h"
#include "maildir-sync.h"
#include "index-mail.h"

#include <sys/stat.h>

#define MAILDIR_SUBFOLDER_FILENAME "maildirfolder"

struct maildir_mailbox_list_context {
	union mailbox_list_module_context module_ctx;
};

extern struct mail_storage maildir_storage;
extern struct mailbox maildir_mailbox;

static struct event_category event_category_maildir = {
	.name = "maildir",
	.parent = &event_category_storage,
};

static MODULE_CONTEXT_DEFINE_INIT(maildir_mailbox_list_module,
				  &mailbox_list_module_register);
static const char *maildir_subdirs[] = { "cur", "new", "tmp" };

static void maildir_mailbox_close(struct mailbox *box);

static struct mail_storage *maildir_storage_alloc(void)
{
	struct maildir_storage *storage;
	pool_t pool;

	pool = pool_alloconly_create("maildir storage", 512+256);
	storage = p_new(pool, struct maildir_storage, 1);
	storage->storage = maildir_storage;
	storage->storage.pool = pool;
	return &storage->storage;
}

static int
maildir_storage_create(struct mail_storage *_storage, struct mail_namespace *ns,
		       const char **error_r ATTR_UNUSED)
{
	struct maildir_storage *storage = MAILDIR_STORAGE(_storage);
	struct mailbox_list *list = ns->list;
	const char *dir;

	if (settings_get(_storage->event, &maildir_setting_parser_info, 0,
			 &storage->set, error_r) < 0)
		return -1;

	storage->temp_prefix = p_strdup(_storage->pool,
					mailbox_list_get_temp_prefix(list));

	if (list->mail_set->mail_control_path[0] == '\0' &&
	    list->mail_set->mail_inbox_path[0] == '\0' &&
	    (ns->flags & NAMESPACE_FLAG_INBOX_ANY) != 0) {
		/* put the temp files into tmp/ directory preferably */
		storage->temp_prefix = p_strconcat(_storage->pool, "tmp/",
						   storage->temp_prefix, NULL);
		dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_DIR);
	} else {
		/* control dir should also be writable */
		dir = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_CONTROL);
	}
	_storage->temp_path_prefix = p_strconcat(_storage->pool, dir, "/",
						 storage->temp_prefix, NULL);
	return 0;
}

static void maildir_storage_destroy(struct mail_storage *_storage)
{
	struct maildir_storage *storage = MAILDIR_STORAGE(_storage);

	settings_free(storage->set);
	index_storage_destroy(_storage);
}

static const char *
maildir_storage_find_root_dir(const struct mail_namespace *ns)
{
	struct event *event = ns->user->event;
	const char *home, *path;

	/* we'll need to figure out the maildir location ourself.
	   It's ~/Maildir unless we are chrooted. */
	if (ns->owner != NULL &&
	    mail_user_get_home(ns->owner, &home) > 0) {
		path = t_strconcat(home, "/Maildir", NULL);
		if (i_faccessat2(AT_FDCWD, path, R_OK | W_OK | X_OK, AT_EACCESS) == 0) {
			e_debug(event,
				"maildir autodetect: root exists (%s)", path);
			return path;
		}
		e_debug(event,
			"maildir autodetect: access(%s, rwx): failed: %m", path);
	} else {
		e_debug(event, "maildir autodetect: Home directory not set");
		if (i_faccessat2(AT_FDCWD, "/cur", R_OK | W_OK | X_OK, AT_EACCESS) == 0) {
			e_debug(event,
				"maildir autodetect: /cur exists, assuming chroot");
			return "/";
		}
	}
	return NULL;
}

static bool
maildir_storage_autodetect(const struct mail_namespace *ns,
			   const struct mail_storage_settings *mail_set,
			   const char **root_path_r,
			   const char **inbox_path_r ATTR_UNUSED)
{
	struct event *event = ns->user->event;
	struct stat st;
	const char *path, *root_dir;

	if (mail_set->mail_path[0] != '\0')
		root_dir = mail_set->mail_path;
	else {
		root_dir = maildir_storage_find_root_dir(ns);
		if (root_dir == NULL) {
			e_debug(event, "maildir autodetect: couldn't find root dir");
			return FALSE;
		}
	}

	path = t_strconcat(root_dir, "/cur", NULL);
	if (stat(path, &st) < 0) {
		e_debug(event, "maildir autodetect: stat(%s) failed: %m", path);
		return FALSE;
	}

	if (!S_ISDIR(st.st_mode)) {
		e_debug(event, "maildir autodetect: %s not a directory", path);
		return FALSE;
	}

	*root_path_r = root_dir;
	return TRUE;
}

static int
mkdir_verify(struct mailbox *box, const char *dir, bool verify)
{
	const struct mailbox_permissions *perm;
	struct stat st;

	if (verify) {
		if (stat(dir, &st) == 0)
			return 0;

		if (errno != ENOENT) {
			mailbox_set_critical(box, "stat(%s) failed: %m", dir);
			return -1;
		}
	}

	perm = mailbox_get_permissions(box);
	if (mkdir_parents_chgrp(dir, perm->dir_create_mode,
				perm->file_create_gid,
				perm->file_create_gid_origin) == 0)
		return 0;

	if (errno == EEXIST) {
		if (verify)
			return 0;
		mail_storage_set_error(box->storage, MAIL_ERROR_EXISTS,
				       "Mailbox already exists");
	} else if (errno == ENOENT) {
		mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
			"Mailbox was deleted while it was being created");
	} else if (ENOACCESS(errno)) {
		if (box->list->ns->type == MAIL_NAMESPACE_TYPE_SHARED) {
			/* shared namespace, don't log permission errors */
			mail_storage_set_error(box->storage, MAIL_ERROR_PERM,
					       MAIL_ERRSTR_NO_PERMISSION);
			return -1;
		}
		mailbox_set_critical(box, "%s",
			mail_error_create_eacces_msg("mkdir", dir));
	} else {
		mailbox_set_critical(box, "mkdir(%s) failed: %m", dir);
	}
	return -1;
}

static int maildir_check_tmp(struct mail_storage *storage, const char *dir)
{
	unsigned int interval = storage->set->mail_temp_scan_interval;
	const char *path;
	struct stat st;

	/* if tmp/ directory exists, we need to clean it up once in a while */
	path = t_strconcat(dir, "/tmp", NULL);
	if (stat(path, &st) < 0) {
		if (errno == ENOENT || errno == ENAMETOOLONG)
			return 0;
		if (ENOACCESS(errno)) {
			mail_storage_set_critical(storage, "%s",
				mail_error_eacces_msg("stat", path));
			return -1;
		}
		mail_storage_set_critical(storage, "stat(%s) failed: %m", path);
		return -1;
	}

	if (interval == 0) {
		/* disabled */
	} else if (st.st_atime > st.st_ctime + MAILDIR_TMP_DELETE_SECS) {
		/* the directory should be empty. we won't do anything
		   until ctime changes. */
	} else if (st.st_atime < ioloop_time - (time_t)interval) {
		/* time to scan */
		(void)unlink_old_files(path, "",
				       ioloop_time - MAILDIR_TMP_DELETE_SECS);
	}
	return 1;
}

/* create or fix maildir, ignore if it already exists */
static int create_maildir_subdirs(struct mailbox *box, bool verify)
{
	const char *path, *box_path;
	unsigned int i;
	enum mail_error error;
	int ret = 0;

	if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
				&box_path) < 0)
		return -1;

	for (i = 0; i < N_ELEMENTS(maildir_subdirs); i++) {
		path = t_strconcat(box_path, "/", maildir_subdirs[i], NULL);
		if (mkdir_verify(box, path, verify) < 0) {
			error = mailbox_get_last_mail_error(box);
			if (error != MAIL_ERROR_EXISTS)
				return -1;
			/* try to create all of the directories in case one
			   of them doesn't exist */
			ret = -1;
		}
	}
	return ret;
}

static void maildir_lock_touch_timeout(struct maildir_mailbox *mbox)
{
	(void)maildir_uidlist_lock_touch(mbox->uidlist);
}

static struct mailbox *
maildir_mailbox_alloc(struct mail_storage *storage, struct mailbox_list *list,
		      const char *vname, enum mailbox_flags flags)
{
	struct maildir_mailbox *mbox;
	pool_t pool;

	pool = pool_alloconly_create("maildir mailbox", 1024*3);
	mbox = p_new(pool, struct maildir_mailbox, 1);
	mbox->box = maildir_mailbox;
	mbox->box.pool = pool;
	mbox->box.storage = storage;
	mbox->box.list = list;
	mbox->box.mail_vfuncs = &maildir_mail_vfuncs;
	mbox->maildir_list_index_ext_id = (uint32_t)-1;

	index_storage_mailbox_alloc(&mbox->box, vname, flags, MAIL_INDEX_PREFIX);

	mbox->storage = MAILDIR_STORAGE(storage);
	return &mbox->box;
}

static int maildir_mailbox_open_existing(struct mailbox *box)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);

	mbox->uidlist = maildir_uidlist_init(mbox);
	mbox->keywords = maildir_keywords_init(mbox);

	if ((box->flags & MAILBOX_FLAG_KEEP_LOCKED) != 0) {
		if (maildir_uidlist_lock(mbox->uidlist) <= 0) {
			maildir_mailbox_close(box);
			return -1;
		}
		mbox->keep_lock_to = timeout_add(MAILDIR_LOCK_TOUCH_SECS * 1000,
						 maildir_lock_touch_timeout,
						 mbox);
	}

	if (index_storage_mailbox_open(box, FALSE) < 0) {
		maildir_mailbox_close(box);
		return -1;
	}

	mbox->maildir_ext_id =
		mail_index_ext_register(mbox->box.index, "maildir",
					sizeof(mbox->maildir_hdr), 0, 0);
	return 0;
}

static bool maildir_storage_is_readonly(struct mailbox *box)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);

	if (index_storage_is_readonly(box))
		return TRUE;

	if (maildir_is_backend_readonly(mbox)) {
		/* return read-only only if there are no private flags
		   (that are stored in index files) */
		if (mailbox_get_private_flags_mask(box) == 0)
			return TRUE;
	}
	return FALSE;
}

static int
maildir_mailbox_exists(struct mailbox *box, bool auto_boxes,
		       enum mailbox_existence *existence_r)
{
	if (auto_boxes && mailbox_is_autocreated(box)) {
		*existence_r = MAILBOX_EXISTENCE_SELECT;
		return 0;
	}

	return index_storage_mailbox_exists_full(box, "cur", existence_r);
}

static bool maildir_has_any_subdir(const char *box_path,
				   const char **error_path)
{
	const char *path;
	unsigned int i;
	struct stat st;

	for (i = 0; i < N_ELEMENTS(maildir_subdirs); i++) {
		path = t_strconcat(box_path, "/", maildir_subdirs[i], NULL);
		if (stat(path, &st) == 0) {
			/* Return if there is any subdir */
			return TRUE;
		} else if (errno == ENOENT || errno == ENAMETOOLONG) {
			/* Some subdirs may not exist. */
		} else {
			*error_path = path;
			return FALSE;
		}
	}

	return FALSE;
}

static bool maildir_is_selectable(const char *box_path,
				  const char *root_dir,
				  const char **error_path_r)
{
	struct stat st;

	*error_path_r = box_path;
	if (strcmp(box_path, root_dir) == 0)
		return maildir_has_any_subdir(box_path, error_path_r);
	else
		return stat(box_path, &st) == 0;
}

static int maildir_mailbox_open(struct mailbox *box)
{
	const char *box_path = mailbox_get_path(box);
	const char *root_dir;
	int ret;

	/* begin by checking if tmp/ directory exists and if it should be
	   cleaned up. */
	ret = maildir_check_tmp(box->storage, box_path);
	if (ret > 0) {
		/* exists */
		return maildir_mailbox_open_existing(box);
	}
	if (ret < 0)
		return -1;

	/* tmp/ directory doesn't exist. does the maildir? autocreate missing
	   dirs only with Maildir++ and imapdir layouts. */
	if (strcmp(box->list->name, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) != 0 &&
	    strcmp(box->list->name, MAILBOX_LIST_NAME_IMAPDIR) != 0) {
		mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
			T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
		return -1;
	}
	root_dir = mailbox_list_get_root_forced(box->list,
						MAILBOX_LIST_PATH_TYPE_MAILBOX);

	/* This code path is executed only for Maildir++ and imapdir layouts,
	   which don't support \Noselect mailboxes. If the mailbox root
	   directory exists, automatically create any missing cur/new/tmp
	   directories. Otherwise the mailbox would show up as selectable
	   in the mailbox list, but not actually be selectable.

	   As a special case we don't do this when the mailbox root directory
	   is the same as the namespace root directory. This especially means
	   that we don't autocreate Maildir INBOX when ~/Maildir directory
	   exists. Instead, we return that mailbox doesn't exist, so the
	   caller goes to the INBOX autocreation code path similarly as with
	   other mailboxes. This is needed e.g. for welcome plugin to work. */
	const char *error_path;
	if (maildir_is_selectable(box_path, root_dir, &error_path)) {
		/* yes, we'll need to create the missing dirs */
		if (create_maildir_subdirs(box, TRUE) < 0)
			return -1;

		return maildir_mailbox_open_existing(box);
	}

	if (errno == ENOENT || errno == ENAMETOOLONG) {
		mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
			T_MAIL_ERR_MAILBOX_NOT_FOUND(box->vname));
		return -1;
	} else {
		mailbox_set_critical(box, "stat(%s) failed: %m", error_path);
		return -1;
	}
}

static int maildir_create_shared(struct mailbox *box)
{
	const struct mailbox_permissions *perm = mailbox_get_permissions(box);
	const char *path;
	mode_t old_mask;
	int fd, ret;

	ret = mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_MAILBOX,
				  &path);
	if (ret < 0)
		return -1;
	i_assert(ret > 0);

	old_mask = umask(0);
	path = t_strconcat(path, "/dovecot-shared", NULL);
	fd = open(path, O_WRONLY | O_CREAT, perm->file_create_mode);
	umask(old_mask);

	if (fd == -1) {
		mailbox_set_critical(box, "open(%s) failed: %m", path);
		return -1;
	}

	if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
		if (errno == EPERM) {
			mailbox_set_critical(box, "%s",
				eperm_error_get_chgrp("fchown", path,
					perm->file_create_gid,
					perm->file_create_gid_origin));
		} else {
			mailbox_set_critical(box,
				"fchown(%s) failed: %m", path);
		}
	}
	i_close_fd(&fd);
	return 0;
}

static int
maildir_mailbox_update(struct mailbox *box, const struct mailbox_update *update)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
	struct maildir_uidlist *uidlist;
	bool locked = FALSE;
	int ret = 0;

	if (!box->opened) {
		if (mailbox_open(box) < 0)
			return -1;
	}
	uidlist = mbox->uidlist;

	if (update->uid_validity != 0 || update->min_next_uid != 0 ||
	    !guid_128_is_empty(update->mailbox_guid)) {
		if (maildir_uidlist_lock(uidlist) <= 0)
			return -1;

		locked = TRUE;
		if (!guid_128_is_empty(update->mailbox_guid))
			maildir_uidlist_set_mailbox_guid(uidlist, update->mailbox_guid);
		if (update->uid_validity != 0)
			maildir_uidlist_set_uid_validity(uidlist, update->uid_validity);
		if (update->min_next_uid != 0) {
			maildir_uidlist_set_next_uid(uidlist, update->min_next_uid,
						     FALSE);
		}
		ret = maildir_uidlist_update(uidlist);
	}
	if (ret == 0)
		ret = index_storage_mailbox_update(box, update);
	if (locked)
		maildir_uidlist_unlock(uidlist);
	return ret;
}

static int maildir_create_maildirfolder_file(struct mailbox *box)
{
	const struct mailbox_permissions *perm;
	const char *path;
	mode_t old_mask;
	int fd;

	/* Maildir++ spec wants that maildirfolder named file is created for
	   all subfolders. Do this only with Maildir++ layout. */
	if (strcmp(box->list->name, MAILBOX_LIST_NAME_MAILDIRPLUSPLUS) != 0)
		return 0;
	perm = mailbox_get_permissions(box);

	path = t_strconcat(mailbox_get_path(box),
			   "/"MAILDIR_SUBFOLDER_FILENAME, NULL);
	old_mask = umask(0);
	fd = open(path, O_CREAT | O_WRONLY, perm->file_create_mode);
	umask(old_mask);
	if (fd != -1) {
		/* ok */
	} else if (errno == ENOENT) {
		mail_storage_set_error(box->storage, MAIL_ERROR_NOTFOUND,
			"Mailbox was deleted while it was being created");
		return -1;
	} else {
		mailbox_set_critical(box, "open(%s, O_CREAT) failed: %m", path);
		return -1;
	}

	if (perm->file_create_gid != (gid_t)-1) {
		if (fchown(fd, (uid_t)-1, perm->file_create_gid) == 0) {
			/* ok */
		} else if (errno == EPERM) {
			mailbox_set_critical(box, "%s",
				eperm_error_get_chgrp("fchown", path,
						      perm->file_create_gid,
						      perm->file_create_gid_origin));
		} else {
			mailbox_set_critical(box, "fchown(%s) failed: %m", path);
		}
	}
	i_close_fd(&fd);
	return 0;
}

static int
maildir_mailbox_create(struct mailbox *box, const struct mailbox_update *update,
		       bool directory)
{
	const char *root_dir, *shared_path;
	/* allow physical location to exist when we rebuild list index, this
	   happens with mailbox_list_layout=index only. */
	bool verify = box->storage->rebuilding_list_index;
	struct stat st;
	int ret;

	if ((ret = index_storage_mailbox_create(box, directory)) <= 0)
		return ret;
	ret = 0;
	/* the maildir is created now. finish the creation as best as we can */
	if (create_maildir_subdirs(box, verify) < 0)
		ret = -1;
	if (maildir_create_maildirfolder_file(box) < 0)
		ret = -1;
	/* if dovecot-shared exists in the root dir, copy it to newly
	   created mailboxes */
	root_dir = mailbox_list_get_root_forced(box->list,
						MAILBOX_LIST_PATH_TYPE_MAILBOX);
	shared_path = t_strconcat(root_dir, "/dovecot-shared", NULL);
	if (stat(shared_path, &st) == 0) {
		if (maildir_create_shared(box) < 0)
			ret = -1;
	}
	if (ret == 0 && update != NULL) {
		if (maildir_mailbox_update(box, update) < 0)
			ret = -1;
	}
	return ret;
}

static int
maildir_mailbox_get_metadata(struct mailbox *box,
			     enum mailbox_metadata_items items,
			     struct mailbox_metadata *metadata_r)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);

	if (index_mailbox_get_metadata(box, items, metadata_r) < 0)
		return -1;

	if ((items & MAILBOX_METADATA_GUID) != 0) {
		if (maildir_uidlist_get_mailbox_guid(mbox->uidlist,
						     metadata_r->guid) < 0)
			return -1;
	}
	return 0;
}

static void maildir_mailbox_close(struct mailbox *box)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);

	if (mbox->keep_lock_to != NULL) {
		maildir_uidlist_unlock(mbox->uidlist);
		timeout_remove(&mbox->keep_lock_to);
	}

	if (mbox->flags_view != NULL)
		mail_index_view_close(&mbox->flags_view);
	if (mbox->keywords != NULL)
		maildir_keywords_deinit(&mbox->keywords);
	if (mbox->uidlist != NULL)
		maildir_uidlist_deinit(&mbox->uidlist);
	index_storage_mailbox_close(box);
}

static void maildir_notify_changes(struct mailbox *box)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
	const char *box_path = mailbox_get_path(box);

	if (box->notify_callback == NULL)
		mailbox_watch_remove_all(&mbox->box);
	else {
		mailbox_watch_add(&mbox->box,
			t_strconcat(box_path, "/new", NULL));
		mailbox_watch_add(&mbox->box,
			t_strconcat(box_path, "/cur", NULL));
	}
}

static bool
maildir_is_internal_name(struct mailbox_list *list ATTR_UNUSED,
			 const char *name)
{
	return strcmp(name, "cur") == 0 ||
		strcmp(name, "new") == 0 ||
		strcmp(name, "tmp") == 0;
}

static void maildir_storage_add_list(struct mail_storage *storage ATTR_UNUSED,
				     struct mailbox_list *list)
{
	struct maildir_mailbox_list_context *mlist;

	mlist = p_new(list->pool, struct maildir_mailbox_list_context, 1);
	mlist->module_ctx.super = list->v;

	list->v.is_internal_name = maildir_is_internal_name;
	MODULE_CONTEXT_SET(list, maildir_mailbox_list_module, mlist);
}

uint32_t maildir_get_uidvalidity_next(struct mailbox_list *list)
{
	const char *path;

	path = mailbox_list_get_root_forced(list, MAILBOX_LIST_PATH_TYPE_CONTROL);
	path = t_strconcat(path, "/"MAILDIR_UIDVALIDITY_FNAME, NULL);
	return mailbox_uidvalidity_next(list, path);
}

static enum mail_flags maildir_get_private_flags_mask(struct mailbox *box)
{
	struct maildir_mailbox *mbox = MAILDIR_MAILBOX(box);
	const char *path, *path2;
	struct stat st;

	if (mbox->private_flags_mask_set)
		return mbox->_private_flags_mask;
	mbox->private_flags_mask_set = TRUE;

	path = mailbox_list_get_root_forced(box->list, MAILBOX_LIST_PATH_TYPE_MAILBOX);
	if (box->list->mail_set->mail_index_private_path[0] != '\0') {
		/* private index directory is set. we'll definitely have
		   private flags. */
		mbox->_private_flags_mask = MAIL_SEEN;
	} else if (!mailbox_list_get_root_path(box->list,
					       MAILBOX_LIST_PATH_TYPE_INDEX,
					       &path2) ||
		   strcmp(path, path2) == 0) {
		/* no separate index directory. we can't have private flags,
		   so don't even bother checking if dovecot-shared exists */
	} else {
		path = t_strconcat(mailbox_get_path(box),
				   "/dovecot-shared", NULL);
		if (stat(path, &st) == 0)
			mbox->_private_flags_mask = MAIL_SEEN;
	}
	return mbox->_private_flags_mask;
}

bool maildir_is_backend_readonly(struct maildir_mailbox *mbox)
{
	if (!mbox->backend_readonly_set) {
		const char *box_path = mailbox_get_path(&mbox->box);

		mbox->backend_readonly_set = TRUE;
		if (i_faccessat2(AT_FDCWD, t_strconcat(box_path, "/cur", NULL), W_OK, AT_EACCESS) < 0 &&
		    ENOACCESS(errno))
			mbox->backend_readonly = TRUE;
	}
	return mbox->backend_readonly;
}

struct mail_storage maildir_storage = {
	.name = MAILDIR_STORAGE_NAME,
	.class_flags = MAIL_STORAGE_CLASS_FLAG_FILE_PER_MSG |
		MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_GUIDS |
		MAIL_STORAGE_CLASS_FLAG_HAVE_MAIL_SAVE_GUIDS |
		MAIL_STORAGE_CLASS_FLAG_BINARY_DATA,
	.event_category = &event_category_maildir,
	.set_info = &maildir_setting_parser_info,

	.v = {
		maildir_storage_alloc,
		maildir_storage_create,
		maildir_storage_destroy,
		maildir_storage_add_list,
		maildir_storage_autodetect,
		maildir_mailbox_alloc,
		NULL,
		mail_storage_list_index_rebuild,
	}
};

struct mailbox maildir_mailbox = {
	.v = {
		maildir_storage_is_readonly,
		index_storage_mailbox_enable,
		maildir_mailbox_exists,
		maildir_mailbox_open,
		maildir_mailbox_close,
		index_storage_mailbox_free,
		maildir_mailbox_create,
		maildir_mailbox_update,
		index_storage_mailbox_delete,
		index_storage_mailbox_rename,
		index_storage_get_status,
		maildir_mailbox_get_metadata,
		index_storage_set_subscribed,
		index_storage_attribute_set,
		index_storage_attribute_get,
		index_storage_attribute_iter_init,
		index_storage_attribute_iter_next,
		index_storage_attribute_iter_deinit,
		maildir_list_index_has_changed,
		maildir_list_index_update_sync,
		maildir_storage_sync_init,
		index_mailbox_sync_next,
		index_mailbox_sync_deinit,
		NULL,
		maildir_notify_changes,
		index_transaction_begin,
		index_transaction_commit,
		index_transaction_rollback,
		maildir_get_private_flags_mask,
		index_mail_alloc,
		index_storage_search_init,
		index_storage_search_deinit,
		index_storage_search_next_nonblock,
		index_storage_search_next_update_seq,
		index_storage_search_next_match_mail,
		maildir_save_alloc,
		maildir_save_begin,
		maildir_save_continue,
		maildir_save_finish,
		maildir_save_cancel,
		maildir_copy,
		maildir_transaction_save_commit_pre,
		maildir_transaction_save_commit_post,
		maildir_transaction_save_rollback,
		index_storage_is_inconsistent
	}
};
